/**
 * Copyright (c) 2008, George Taylor
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without modification, are permitted 
 * provided that the following conditions are met:
 * 
 *   * Redistributions of source code must retain the above copyright notice, this list of 
 *     conditions and the following disclaimer.
 *   * Redistributions in binary form must reproduce the above copyright notice, this list of 
 *     conditions and the following disclaimer in the documentation and/or other materials 
 *     provided with the distribution.
 *   * Neither the name of the copyright holder's organization nor the names of its contributors 
 *     may be used to endorse or promote products derived from this software without specific 
 *     prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR 
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 
 * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT 
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package uk.co.taylorconsulting.annoweb.web.builder;

import java.io.File;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.HashMap;

import org.apache.log4j.Logger;

import uk.co.taylorconsulting.annoweb.annotation.action.Action;
import uk.co.taylorconsulting.annoweb.annotation.action.Default;
import uk.co.taylorconsulting.annoweb.annotation.action.Init;
import uk.co.taylorconsulting.annoweb.annotation.exception.CatchException;
import uk.co.taylorconsulting.annoweb.annotation.exception.CatchExceptions;
import uk.co.taylorconsulting.annoweb.annotation.form.Form;
import uk.co.taylorconsulting.annoweb.annotation.form.validation.Validator;
import uk.co.taylorconsulting.annoweb.annotation.security.Login;
import uk.co.taylorconsulting.annoweb.annotation.security.NotAuthorised;
import uk.co.taylorconsulting.annoweb.annotation.security.Role;
import uk.co.taylorconsulting.annoweb.annotation.security.Roles;
import uk.co.taylorconsulting.annoweb.annotation.security.SecurityHandler;
import uk.co.taylorconsulting.annoweb.annotation.security.SecurityProcessor;
import uk.co.taylorconsulting.annoweb.annotation.web.DefaultForward;
import uk.co.taylorconsulting.annoweb.annotation.web.Forward;
import uk.co.taylorconsulting.annoweb.annotation.web.Forwards;
import uk.co.taylorconsulting.annoweb.annotation.web.GlobalDefaultForward;
import uk.co.taylorconsulting.annoweb.annotation.web.GlobalForward;
import uk.co.taylorconsulting.annoweb.annotation.web.GlobalForwards;
import uk.co.taylorconsulting.annoweb.annotation.web.QuickLink;
import uk.co.taylorconsulting.annoweb.annotation.web.Web;
import uk.co.taylorconsulting.annoweb.web.builder.data.ActionHolder;
import uk.co.taylorconsulting.annoweb.web.builder.data.FormData;
import uk.co.taylorconsulting.annoweb.web.builder.data.Globals;
import uk.co.taylorconsulting.annoweb.web.builder.data.Site;
import uk.co.taylorconsulting.annoweb.web.builder.data.SiteHolder;
import uk.co.taylorconsulting.annoweb.web.controller.WebFramework;

public class WebBuilder {
	private static Logger LOG = Logger.getLogger(WebBuilder.class);
	
	public void process(String classpath, String ext, HashMap<String, Site> sites) {
		String[] bits = classpath.split(System.getProperty("path.separator"));
		if (sites.size() == 0) {
			sites.put("xxDefaultxx", new Site("xxDefaultxx", null, ext));
		}
		Globals globals = new Globals();
		for (Site s : sites.values()) {
			s.setGlobals(globals);
		}
		if (LOG.isDebugEnabled()) {
			LOG.debug("Building Web app with Classpath = " + classpath);
		}
		for (String s : bits) {
			processPath(sites, s, globals);
		}
	}
	
	private void processPath(HashMap<String, Site> sites, String path, Globals globals) {
		try {
			File f = new File(path);
			if (f.exists()) {
				if (f.isDirectory()) {
					processDir(f, sites, globals);
				} else if (f.isFile()) {
					processFile(f, sites, globals);
				}
			}
		} catch (java.lang.Exception e) {
			LOG.warn("Error building site", e);
		}
	}
	
	private void processDir(File dir, HashMap<String, Site> sites, Globals globals) {
		if (LOG.isDebugEnabled()) {
			LOG.debug("Processing dir: " + dir.getAbsolutePath());
		}
		for (File f : dir.listFiles()) {
			if (f.isDirectory()) {
				processDir(f, sites, globals);
			}
			processFile(f, sites, globals);
		}
	}
	
	private void processFile(File file, HashMap<String, Site> sites, Globals globals) {
		String name = file.getName().toLowerCase();
		if (LOG.isDebugEnabled()) {
			LOG.debug("Processing file: " + file.getAbsoluteFile().toString());
		}
		if (name.endsWith(".class")) {
			processClass(file, sites, globals);
		}
	}
	
	private void processClass(File file, HashMap<String, Site> sites, Globals globals) {
		Class<?> clazz = getCanonacleName(file);
		Site tmp = new Site("tmp", null);
		tmp.setGlobals(globals);
		if (LOG.isDebugEnabled()) {
			LOG.debug("Processing class: " + clazz.getCanonicalName());
		}
		Site classSite = null;
		for (Annotation a : clazz.getAnnotations()) {
			classSite = processClassAnnotation(clazz, a, sites, globals, classSite, tmp);
		}
		if (classSite == null) {
			Site defaultSite = sites.get("xxDefaultxx");
			defaultSite.merge(tmp);
			classSite = defaultSite;
		} else {
			if (sites.containsKey(classSite.getName())) {
				tmp = sites.get(classSite.getName());
				tmp.merge(classSite);
				classSite = tmp;
			} else {
				sites.put(classSite.getName(), classSite);
			}
		}
		tmp = new Site("tmp", null);
		tmp.setGlobals(globals);
		for (Method m : clazz.getMethods()) {
			Site methodSite = null;
			Action action = m.getAnnotation(Action.class);
			if (action != null) {
				ActionHolder ah = tmp.addAction((Action) action, m);
				for (Annotation a : m.getAnnotations()) {
					methodSite = processMethodAnnotation(clazz, m, action, a, sites, globals, methodSite, tmp);
				}
				if (methodSite == null) {
					classSite.merge(tmp);
				} else {
					sites.put(methodSite.getName(), methodSite);
				}
				if (m.getAnnotation(Login.class) != null) {
					globals.setLogin(ah);
				}
				if (m.getAnnotation(NotAuthorised.class) != null) {
					globals.setNotAuthed(ah);
				}
				if (m.getAnnotation(QuickLink.class) != null) {
					globals.addQuickLink(ah, m.getAnnotation(QuickLink.class));
				}
			} else if (m.getAnnotation(CatchException.class) != null) {
				CatchException ce = m.getAnnotation(CatchException.class);
				globals.addExceptionHandler(m, ce);
			} else if (m.getAnnotation(CatchExceptions.class) != null) {
				CatchExceptions ces = m.getAnnotation(CatchExceptions.class);
				for (CatchException ce : ces.value()) {
					globals.addExceptionHandler(m, ce);
				}
			}
		}
	}
	
	private Site processMethodAnnotation(Class<?> clazz, Method m, Action action, Annotation a, HashMap<String, Site> sites, Globals globals, Site methodSite, Site tmp) {
		Site site = getSite(methodSite, tmp);
		if (a instanceof Action) {} else if (a instanceof Default) {
			site.setDefaultAction(m);
		} else if (a instanceof CatchException) {
			globals.addExceptionHandler(m, (CatchException) a);
		} else if (a instanceof CatchExceptions) {
			for (CatchException ca : ((CatchExceptions) a).value()) {
				globals.addExceptionHandler(m, (CatchException) ca);
			}
		} else if (a instanceof Validator) {
			globals.addValidator((Validator) a, m);
		} else if (a instanceof Role) {
			site.addActionRole(action, (Role) a);
		} else if (a instanceof Roles) {
			for (Role r : ((Roles) a).value()) {
				site.addActionRole(action, r);
			}
		} else if (a instanceof DefaultForward) {
			site.addActionDefaultForward((DefaultForward) a, action);
		} else if (a instanceof Forward) {
			site.addActionForward((Forward) a, action);
		} else if (a instanceof Forwards) {
			for (Forward f : ((Forwards) a).value()) {
				site.addActionForward(f, action);
			}
		} else if (a instanceof Web) {
			if (site == tmp) {
				site.setWeb((Web) a);
				methodSite = site;
			}
		}
		return methodSite;
	}
	
	@SuppressWarnings("unchecked")
	private Site processClassAnnotation(final Class<?> clazz, Annotation annotation, HashMap<String, Site> sites, Globals globals, Site classSite, Site tmp) {
		Site site = getSite(classSite, tmp);
		if (annotation instanceof Init) {
			try {
				if (clazz.getConstructor((Class<?>[]) null) != null) {
					Thread t = new Thread() {
						public void run() {
							try {
								clazz.newInstance();
							} catch (Exception e) {
								LOG.warn("Error while instantiating web init class " + clazz.getCanonicalName(), e);
							}
						}
					};
					t.start();
				}
			} catch (Exception e) {
				LOG.warn("Error while instantiating web init class " + clazz.getCanonicalName(), e);
			}
		} else if (annotation instanceof Form) {
			globals.addForm(clazz);
		} else if (annotation instanceof Role) {
			site.addRole((Role) annotation, clazz);
		} else if (annotation instanceof Roles) {
			for (Role r : ((Roles) annotation).value()) {
				site.addRole(r, clazz);
			}
		} else if (annotation instanceof SecurityHandler) {
			if (SecurityProcessor.class.isAssignableFrom(clazz)) {
				Class<? extends SecurityProcessor> sp = (Class<? extends SecurityProcessor>) clazz;
				globals.addSecurityHandler((SecurityHandler) annotation, sp);
			}
		} else if (annotation instanceof DefaultForward) {
			site.addDefaultForward((DefaultForward) annotation, clazz);
		} else if (annotation instanceof Forward) {
			site.addForward((Forward) annotation, clazz);
		} else if (annotation instanceof Forwards) {
			for (Forward f : ((Forwards) annotation).value()) {
				site.addForward(f, clazz);
			}
		} else if (annotation instanceof GlobalDefaultForward) {
			globals.setDefaultForward((GlobalDefaultForward) annotation);
		} else if (annotation instanceof GlobalForward) {
			globals.addForward((GlobalForward) annotation);
		} else if (annotation instanceof GlobalForwards) {
			for (GlobalForward gf : ((GlobalForwards) annotation).value()) {
				globals.addForward(gf);
			}
		} else if (annotation instanceof Web) {
			if (site == tmp) {
				site.setWeb((Web) annotation);
				classSite = site;
			}
		}
		return classSite;
	}
	
	private Site getSite(Site classSite, Site tmp) {
		if (classSite == null)
			return tmp;
		return classSite;
	}
	
	private Class<?> getCanonacleName(File clazz) {
		String clazzName = clazz.getName();
		if (clazzName.indexOf(".") > 0) {
			clazzName = clazzName.substring(0, clazzName.indexOf("."));
		}
		clazz = clazz.getParentFile();
		while (clazz != null) {
			try {
				return Class.forName(clazzName);
			} catch (Exception e) {}
			clazzName = clazz.getName() + "." + clazzName;
			clazz = clazz.getParentFile();
		}
		return null;
	}
	
	public WebFramework build(HashMap<String, Site> sites, String classpath) {
		String[] bits = classpath.split(System.getProperty("path.separator"));
		File tmpLocation = new File(new File(bits[0]), "tmp");
		HashMap<String, SiteHolder> methods = new HashMap<String, SiteHolder>();
		Globals globals = null;
		for (Site s : sites.values()) {
			for (ActionHolder ah : s.getActions().values()) {
				Method m = ah.getMethod();
				methods.put(m.getDeclaringClass().getCanonicalName() + ":" + m.getName(), new SiteHolder(s, m, m.getDeclaringClass(), ah));
			}
		}
		mkdir(tmpLocation);
		for (Site s : sites.values()) {
			if (globals == null) {
				globals = s.getGlobals();
			}
			SiteBuilder.buildSite(s, tmpLocation, methods);
		}
		for (FormData fd : globals.getForms()) {
			FormBuilder.buildFormHelper(fd, tmpLocation, globals);
		}
		QuicklinkBuilder.buildSite(sites, tmpLocation);
		ControllerBuilder.buildSite(sites, tmpLocation);
		GlobalDataBuilder.buildSite(sites, tmpLocation);
		SecurityHandlerBuilder.buildSite(sites, tmpLocation);
		Compiler c = new Compiler();
		return c.compile(tmpLocation);
	}
	
	private void mkdir(File tmpLocation) {
		if (!tmpLocation.exists()) {
			mkdir(tmpLocation.getParentFile());
			tmpLocation.mkdir();
		}
	}
}
